Security News
JSR Working Group Kicks Off with Ambitious Roadmap and Plans for Open Governance
At its inaugural meeting, the JSR Working Group outlined plans for an open governance model and a roadmap to enhance JavaScript package management.
@safe-global/safe-apps-sdk
Advanced tools
SDK developed to integrate third-party apps with Safe app.
@safe-global/safe-apps-sdk is a JavaScript SDK that allows developers to interact with Safe (formerly Gnosis Safe) applications. It provides a set of tools to build decentralized applications (dApps) that can interact with Safe smart contracts, enabling functionalities such as transaction creation, signing, and execution within the Safe ecosystem.
Initialize SDK
This code snippet demonstrates how to initialize the Safe Apps SDK. This is the first step to start interacting with Safe applications.
const SafeAppsSDK = require('@safe-global/safe-apps-sdk');
const sdk = new SafeAppsSDK();
Get Safe Info
This feature allows you to retrieve information about the Safe, such as its address, owners, and threshold. The code snippet shows how to get this information and log it to the console.
sdk.safe.getInfo().then((safeInfo) => {
console.log(safeInfo);
});
Create Transaction
This feature allows you to create and send a transaction from the Safe. The code snippet demonstrates how to create a transaction object and send it using the SDK.
const transaction = {
to: '0xRecipientAddress',
value: '1000000000000000000', // 1 ETH in wei
data: '0x',
};
sdk.txs.send({ txs: [transaction] }).then((response) => {
console.log(response);
});
Sign Message
This feature allows you to sign a message with the Safe. The code snippet shows how to sign a message and log the signature to the console.
const message = 'Hello, Safe!';
sdk.safe.signMessage(message).then((signature) => {
console.log(signature);
});
Web3.js is a collection of libraries that allow you to interact with a local or remote Ethereum node using HTTP, IPC, or WebSocket. While it provides a broader range of functionalities for interacting with Ethereum, it does not offer the specialized tools for Safe applications that @safe-global/safe-apps-sdk does.
Ethers.js is a library for interacting with the Ethereum blockchain and its ecosystem. It is similar to Web3.js but is known for its smaller size and better documentation. Like Web3.js, it does not provide the specialized functionalities for Safe applications that @safe-global/safe-apps-sdk offers.
Gnosis Safe is a smart contract wallet that requires a minimum number of people to approve a transaction before it can occur (multisig). While it provides the core functionalities for Safe, the @safe-global/safe-apps-sdk offers a more developer-friendly interface for building dApps that interact with Safe.
Software development kit to integrate third-party applications (Safe Apps) with Safe (https://app.safe.global/).
yarn add @safe-global/safe-apps-sdk
npm install @safe-global/safe-apps-sdk
yarn install
yarn build
npm install
npm build
There are differences in arguments for sed
unix utility on MacOS and Linux:
For MacOS it should be:
"format-dist": "sed -i '' 's/\"files\":/\"_files\":/' dist/package.json"
And for Linux:
"format-dist": "sed -i 's/\"files\":/\"_files\":/' dist/package.json",
Apps built with the Safe Apps SDK are meant to be run in an iframe inside the Safe Web UI.
This library exposes a class as a default export. It accepts an optional options object:
allowedDomains
- Array of regular expressions for origins to accept messages from. If not passed, accepts
messages from all domains
debug
- Boolean. If enabled, it will log outgoing/incoming messages.
import SafeAppsSDK from '@safe-global/safe-apps-sdk';
type Opts = {
allowedDomains?: RegExp[];
debug?: boolean;
};
const opts: Opts = {
allowedDomains: [/^app\.safe\.global$/],
debug: false,
};
const appsSdk = new SafeAppsSDK(opts);
The instance allows you to interact with the Safe application.
Safe information can be obtained by calling .safe.getInfo()
const safe = await appsSdk.safe.getInfo();
// {
// "safeAddress": "0x2fC97b3c7324EFc0BeC094bf75d5dCdFEb082C53",
// "chainId": 4
// }
Chain information can be obtained by calling .safe.getChainInfo()
const chain = await appsSdk.safe.getChainInfo();
// {
// chainName: "Rinkeby",
// chainId: "4",
// shortName: "rin",
// nativeCurrency: {
// name: "Ether",
// symbol: "ETH",
// decimals: 18,
// logoUri:
// "https://safe-transaction-assets.gnosis-safe.io/chains/4/currency_logo.png",
// },
// blockExplorerUriTemplate: {
// address: "https://rinkeby.etherscan.io/address/{{address}}",
// txHash: "https://rinkeby.etherscan.io/tx/{{txHash}}",
// api: "https://api-rinkeby.etherscan.io/api?module={{module}}&action={{action}}&address={{address}}&apiKey={{apiKey}}",
// },
// }
Safe Balances can be obtained by calling .safe.experimental_getBalances()
. This method is experimental and may be changed to return paginated results.
It accepts an optional object argument with currency
property.
currency
- ISO 4217 currency code as string
const safe = await appsSdk.safe.experimental_getBalances({ currency: 'rub' });
// {
// "fiatTotal": "0",
// "items": [
// {
// "tokenInfo": {
// "type": "ETHER",
// "address": "0x0000000000000000000000000000000000000000",
// "decimals": 18,
// "symbol": "ETH",
// "name": "Ether",
// "logoUri": null
// },
// "balance": "0",
// "fiatBalance": "0",
// "fiatConversion": "222297.29"
// }
// ]
// }
For detailed returned data types, please check our type declaration file
Sending a TX through the Safe is as simple as invoking .txs.send()
// Create a web3 instance
const web3 = new Web3('https://goerli.infura.io/v3/{YOUR_API_KEY}');
const contract = new web3.eth.Contract(abi, contractAddress);
const txs = [
{
to: someAddress,
value: 0,
data: contract.methods.someMethod().encodeABI(),
},
{
to: someAddress2,
value: 0,
data: contract.methods.someOtherMethod().encodeABI(),
},
];
const params = {
safeTxGas: 500000,
};
try {
const txs = await appsSdk.txs.send({ txs, params });
// { safeTxHash: '0x...' }
} catch (err) {
console.error(err.message);
}
Note:
value
accepts a number or a string as a decimal or hex number.
Signing a message off-chain first requires dispatching a safe_setSettings
RPC call (via sdk.eth.setSafeSettings()
) with the offChainSigning
flag set to true
. Then the relevant signMessage
/signTypedData
needs to be called, proposing a message to our services that can then be approved by Safe owners.
const settings = {
offChainSigning: true,
};
const currentSettings = await appsSdk.eth.setSafeSettings([settings]);
const message = "I'm the owner of wallet 0x000000";
const hash = await sdk.txs.signMessage(message);
// { messageHash: '0x...' }
const typedMessage = {
...
}
const hash = await sdk.txs.signTypedMessage(typedMessage);
// { messageHash: '0x...' }
Signing returns the messageHash
of the proposed SafeMessage
which can be used to fetch the off-chain signature with.
const offChainSignature = await sdk.safe.getOffChainSignature(messageHash);
// '0x123'
The returned signature will either be an empty string or valid one once the required number of Safe owners have confirmed the message.
To validate the signature, use sdk.safe.isMessageSigned()
, passing the signature as the second argument.
const message = "I'm the owner of wallet 0x000000";
const messageIsSigned = await sdk.safe.isMessageSigned(message, signature);
Because the Safe is a smart contract wallet, it doesn't have a private key that the wallet can use to sign messages. Instead, we have a library to sign messages, and the validation logic follows EIP-1271 - Standard Signature Validation Method for Contracts. Signing a message with the Safe requires sending a Safe transaction that needs to be approved by Safe owners. To dive into the smart contract implementation, you can start with library tests in the safe-contracts repo.
To trigger the transaction to sign a message, you can use sdk.txs.signMessage()
or sdk.txs.signTypedMessage()
.
const message = "I'm the owner of wallet 0x000000";
const tx = await sdk.txs.signMessage(message);
// { safeTxHash: '0x...' }
const typedMessage = {
...
}
const typedTx = await sdk.txs.signTypedMessage(typedMessage);
// { safeTxHash: '0x...' }
The non-typed message will be hashed using EIP-191. To calculate the hash, you can use sdk.safe.calculateMessageHash()
.
In case of a typed message, it will be hashed according to EIP-712 standard. To calculate message hash, you can use sdk.safe.calculateTypedMessageHash()
:
const messageHash = sdk.safe.calculateMessageHash(message);
const typedMessageHash = sdk.safe.calculateTypedMessageHash(typedMessage);
To validate if the message is signed, use sdk.safe.isMessageSigned()
const message = "I'm the owner of wallet 0x000000";
const messageIsSigned = await sdk.safe.isMessageSigned(message);
Once you received safe transaction hash, you might want to get the status of the transaction (was it executed? how many confirmations does it have?):
const tx = await sdk.txs.getBySafeTxHash(safeTxHash);
It will return a GatewayTransactionDetails structure or throw an error if the backend hasn't synced the transaction yet
The Address Book can be obtained by calling .safe.requestAddressBook()
. This method is restricted so it needs to be approved by the current connected Safe owner if the caller doesn't have granted permissions.
All the flow for making the request is handled by the SDK so developers only need to use this method and the rest will be handled by the SDK and the Web UI.
const addressBook = await appsSdk.safe.requestAddressBook();
// [
// {
// address: '0x0',
// chainId: '4',
// name: 'Entry 1',
// },
// {
// address: '0x1',
// chainId: '4',
// name: 'Entry 2',
// },
// ];
Returns an array of AddressBookItem
Usually you are not going to use these methods because restricted methods as requestAddressBook
handle all the logic for you.
However as part of the internal implementation for the EIP-2255 you now have accessible 2 more methods under the wallet
section in the SDK.
Get permissions
await appsSdk.wallet.getPermissions();
// [
// {
// parentCapability: "requestAddressBook",
// invoker: "https://invoker-dapp.eth",
// date: 1658499292741,
// caveats: [],
// }
// ]
Returns an array of permissions granted to the dapp.
Request Permissions
For requesting permissions you should call the requestPermissions
method that accepts a parameter with an array of requested permissions.
await appsSdk.wallet.requestPermissions([{ requestAddressBook: {} }]);
// [
// {
// parentCapability: "requestAddressBook",
// invoker: "https://invoker-dapp.eth",
// date: 1658499292741,
// caveats: [],
// }
// ]
Returns an array of permissions granted to the dapp. If the permissions are not currently granted then the user will be prompted from the Web UI (only first time) about granting the corresponding permissions.
The SDK exposes some of Ethereum's JSON-RPC API, namely the read methods.
The following methods have an extra default block parameter:
When requests are made that act on the state of ethereum, the last default block parameter determines the height of the block.
The following options are possible for the defaultBlock parameter:
HEX String
- an integer block number
String "earliest"
for the earliest/genesis block
String "latest"
- for the latest mined block (default)
String "pending"
- for the pending state/transactions
Returns the balance of the account of given address.
const balance = await appsSdk.eth.getBalance(['0x...']);
Returns code at a given address.
const code = await appsSdk.eth.getCode(['0x...']);
Returns the value from a storage position at a given address.
const value = await appsSdk.eth.getStorageAt(['0x...', 0]);
Executes a new message call immediately without creating a transaction on the block chain.
const config = {
from: '0x0000000000000000000000000000000000000000',
to: '0x0000000000000000000000000000000000000000',
};
const result = await appsSdk.eth.call([config]);
The transaction call object:
from
- (optional) The address the transaction is sent from.
to
20 Bytes - The address the transaction is directed to.
gas
- (optional) Integer of the gas provided for the transaction execution. eth_call consumes zero gas, but this parameter may be needed by some executions.
gasPrice
- (optional) Integer of the gasPrice used for each paid gas
value
- (optional) Integer of the value sent with this transaction
data
- (optional) Hash of the method signature and encoded parameters. For details see Ethereum Contract ABI in the Solidity documentation
Returns an array of all logs matching a given filter object.
const params = [
{
fromBlock: 11054275,
toBlock: 'latest',
},
];
const logs = await appsSdk.eth.getPastLogs([params]);
The filter options:
fromBlock
- Integer block number, or "latest" for the last mined block or "pending", "earliest" for not yet mined transactions.
toBlock
- Integer block number, or "latest" for the last mined block or "pending", "earliest" for not yet mined transactions.
address
- (optional) Contract address or a list of addresses from which logs should originate.
topics
- (optional) Array of 32 Bytes DATA topics. Topics are order-dependent. Each topic can also be an array of DATA with “or” options.
Returns information about a block by hash.
const hash = '0x1955a9f306903669e295196752b11bc0dee33b48cabdf44b1103b7cea086cae7';
const block = await appsSdk.eth.getBlockByHash([hash, true]);
Parameters
DATA
- Hash of a block.
Boolean
(default: false) - If true it returns the full transaction objects, if false only the hashes of the transactions.
Returns information about a block by block number.
const number = 11054275;
const block = await appsSdk.eth.getBlockByNumber([number]);
Parameters
QUANTITY|TAG
- integer of a block number, or the string "earliest", "latest" or "pending", as in the default block parameter.
Boolean
(default: false) - If true it returns the full transaction objects, if false only the hashes of the transactions.
Returns the information about a transaction requested by transaction hash.
const tx = await appsSdk.eth.getTransactionByHash([
'0x88df016429689c079f3b2f6ad39fa052532c56795b733da78a91ebe6a713944b',
]);
Returns the receipt of a transaction by transaction hash.
Note: That the receipt is not available for pending transactions.
const tx = await appsSdk.eth.getTransactionReceipt([
'0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238',
]);
Sets settings of the currently opened Safe.
Note: Returns the new
SafeSettings
.
const settings = {
offChainSigning: true,
};
const success = await appsSdk.eth.setSafeSettings([settings]);
It is mandatory that your app exposes a manifest.json
file in the root dir with this structure:
{
"name": "YourAppName",
"description": "A description of what your app do",
"icons": [
{
"src": "/myAppIcon.svg",
"type": "image/svg+xml",
"sizes": "any"
}
]
}
Note: icons[n].src it's the public relative path where the Safe will try to load your app icon. For this example, it should be https://yourAppUrl/myAppIcon.svg.
We are following the Manifest spec so you can find more info about how to fill the icons
field there.
As the Safe app is included into the Safe application via an iframe it is required to enable Cross Site Requests by setting the CORS headers when serving the Safe app.
The required headers are:
"Access-Control-Allow-Origin": "\*",
"Access-Control-Allow-Methods": "GET",
"Access-Control-Allow-Headers": "X-Requested-With, content-type, Authorization"
It is possible to use the local React development server. For this you need to set the CORS headers and make sure to use the same protocol (http or https) as the Safe interface.
For this we recommend to use react-app-rewired. To enable the library update the scripts
section in the package.json
:
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test"
},
Additionally, you need to create the config-overrides.js
file in the root of the project to confirgure the CORS headers. The content of the file should be:
/* config-overrides.js */
module.exports = {
// The function to use to create a webpack dev server configuration when running the development
// server with 'npm run start' or 'yarn start'.
// Example: set the dev server to use a specific certificate in https.
devServer: function (configFunction) {
// Return the replacement function for create-react-app to use to generate the Webpack
// Development Server config. "configFunction" is the function that would normally have
// been used to generate the Webpack Development server config - you can use it to create
// a starting configuration to then modify instead of having to create a config from scratch.
return function (proxy, allowedHost) {
// Create the default config by calling configFunction with the proxy/allowedHost parameters
const config = configFunction(proxy, allowedHost);
config.headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET',
'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
};
// Return your customised Webpack Development Server config.
return config;
};
},
};
To enable SSL with react-scripts
it is necessary to set the HTTPS
environment variable to true
. This can be done in the package.json
file by adjusting the scripts
section to:
"scripts": {
"start": "HTTPS=true react-app-rewired start",
},
As in most cases the SSL certificate provided by react-scripts
is not valid it is required to mark it as trusted in your browser. For this open the Safe App in a separate tab (not in the Safe interface) and accept the certificate/ ignore the warning.
While developing your Safe App you can directly use our production interface for testing it. Some testnets like Goerli are also available there. Once your app is live, even if you are running it locally, you can import it to the Safe application as a custom app. To do so, you should select the "Apps" tab:
Use the Add custom app
button and add your app using a link:
This requires that you have ipfs
installed (Instructions)
yarn build
ipfs add -r build
This library is released under MIT.
FAQs
SDK developed to integrate third-party apps with Safe app.
The npm package @safe-global/safe-apps-sdk receives a total of 195,617 weekly downloads. As such, @safe-global/safe-apps-sdk popularity was classified as popular.
We found that @safe-global/safe-apps-sdk demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
At its inaugural meeting, the JSR Working Group outlined plans for an open governance model and a roadmap to enhance JavaScript package management.
Security News
Research
An advanced npm supply chain attack is leveraging Ethereum smart contracts for decentralized, persistent malware control, evading traditional defenses.
Security News
Research
Attackers are impersonating Sindre Sorhus on npm with a fake 'chalk-node' package containing a malicious backdoor to compromise developers' projects.